/*
* Copyright (c) 2009, 2010, 2011 Daniel Rendall
* This file is part of FractDim.
*
* FractDim is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FractDim is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FractDim. If not, see <http://www.gnu.org/licenses/>
*/
package uk.co.danielrendall.fractdim.app.gui;
import org.apache.batik.swing.JSVGCanvas;
import org.apache.batik.swing.JSVGScrollPane;
import org.apache.batik.swing.gvt.GVTTreeRendererAdapter;
import org.apache.batik.swing.gvt.GVTTreeRendererEvent;
import org.apache.batik.swing.svg.AbstractJSVGComponent;
import org.jdesktop.swingx.JXMultiSplitPane;
import org.jdesktop.swingx.MultiSplitLayout;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.svg.SVGDocument;
import org.w3c.dom.svg.SVGSVGElement;
import uk.co.danielrendall.fractdim.app.model.FractalDocument;
import uk.co.danielrendall.fractdim.calculation.SquareCountingResult;
import uk.co.danielrendall.fractdim.logging.Log;
import uk.co.danielrendall.fractdim.logging.PrettyPrinter;
import uk.co.danielrendall.fractdim.svg.SVGContentGenerator;
import uk.co.danielrendall.fractdim.svg.SVGElementCreator;
import uk.co.danielrendall.mathlib.geom2d.BoundingBox;
import uk.co.danielrendall.mathlib.geom2d.Point;
import uk.co.danielrendall.mathlib.geom2d.Vec;
import javax.swing.*;
import javax.swing.border.EtchedBorder;
import java.awt.*;
import java.io.StringWriter;
import java.util.*;
import java.util.List;
/**
* Created by IntelliJ IDEA.
* User: daniel
* Date: 03-Apr-2010
* Time: 21:08:57
* To change this template use File | Settings | File Templates.
*/
public class FractalPanel extends JLayeredPane {
private final JXMultiSplitPane splitPane;
private final SettingsPanel settingsPanel;
private final JSVGCanvas canvas;
private final ResultPanel resultPanel;
private final JProgressBar progressBar;
private final JPanel progressPanel;
// Maps our identifiers for overlays (e.g. "MAX_GRID") to the actual id of the group element representing that overlay
private final Map<String, String> overlayIdMap;
// Maps the ID of a group element to its desired z-index (note - these should all be negative)
private final Map<String, Integer> zIndexMap;
private final Map<String, BoundingBox> overlayBoundingBoxes;
private BoundingBox rootBoundingBox;
private BoundingBox currentBoundingBox;
private final Queue<Runnable> temporaryRunnableQueue;
private final Object lock = new Object();
// need to know when Batik's update manager is ready - then we can start adding things to the document
private transient boolean updateManagerIsReady = false;
public FractalPanel() {
this.settingsPanel = new SettingsPanel();
canvas = new JSVGCanvas();
canvas.setDocumentState(AbstractJSVGComponent.ALWAYS_DYNAMIC);
getActionMap().setParent(canvas.getActionMap());
JSVGScrollPane canvasScrollPane = new JSVGScrollPane(canvas);
resultPanel = new ResultPanel();
overlayIdMap = new HashMap<String, String>();
zIndexMap = new HashMap<String, Integer>();
overlayBoundingBoxes = new HashMap<String, BoundingBox>();
temporaryRunnableQueue = new LinkedList<Runnable>();
MultiSplitLayout.Split modelRoot = new MultiSplitLayout.Split();
List<MultiSplitLayout.Node> children =
Arrays.asList(new MultiSplitLayout.Leaf("settings"),
new MultiSplitLayout.Divider(),
new MultiSplitLayout.Leaf("svg"),
new MultiSplitLayout.Divider(),
new MultiSplitLayout.Leaf("results"));
modelRoot.setChildren(children);
MultiSplitLayout msl = new MultiSplitLayout(modelRoot);
msl.setFloatingDividers(true);
splitPane = new JXMultiSplitPane(msl);
splitPane.add(settingsPanel, "settings");
splitPane.add(canvasScrollPane, "svg");
splitPane.add(new JScrollPane(resultPanel), "results");
add(splitPane, JLayeredPane.DEFAULT_LAYER);
progressPanel = new JPanel(new BorderLayout());
progressBar = new JProgressBar(0, 100);
progressBar.setPreferredSize(new Dimension(300, 28));
progressBar.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED, SystemColor.controlHighlight, SystemColor.controlShadow));
progressPanel.add(progressBar, BorderLayout.CENTER);
progressPanel.setVisible(false);
add(progressPanel, JLayeredPane.MODAL_LAYER);
}
@Override
public void setBounds(int x, int y, int width, int height) {
splitPane.setBounds(0, 0, width, height);
Point center = new Point (x + width / 2, y + height / 2);
Dimension preferredSize = progressPanel.getPreferredSize();
Point location = center.displace(new Vec(preferredSize.width, preferredSize.height).scale(-0.5d));
progressPanel.setBounds((int)location.x(), (int)location.y(), preferredSize.width, preferredSize.height);
super.setBounds(x, y, width, height);
}
public void updateDocument(FractalDocument doc) {
canvas.addGVTTreeRendererListener(new GVTTreeRendererAdapter() {
@Override
public void gvtRenderingCompleted(GVTTreeRendererEvent e) {
synchronized (lock) {
updateManagerIsReady = true;
for (Iterator<Runnable> it = temporaryRunnableQueue.iterator(); it.hasNext();) {
Runnable next = it.next();
canvas.getUpdateManager().getUpdateRunnableQueue().invokeLater(next);
it.remove();
}
}
}
});
rootBoundingBox = doc.getMetadata().getBoundingBox();
currentBoundingBox = rootBoundingBox;
// put any existing group elements inside a known root group with ZIndex Integer.MAX_VALUE
SVGDocument document = doc.getSvgDoc();
SVGElementCreator creator = new SVGElementCreator(document);
SVGSVGElement root = document.getRootElement();
Element group = creator.createGroup();
NodeList children = root.getChildNodes();
int childCount = children.getLength();
List<Node> nodesToAppendToGroup = new ArrayList<Node>();
for (int i=0; i< childCount; i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
Element childEl = (Element) child;
if ("g".equals(childEl.getTagName())) {
nodesToAppendToGroup.add(childEl);
}
}
}
for (Node node:nodesToAppendToGroup) {
group.appendChild(root.removeChild(node));
}
root.appendChild(group);
zIndexMap.put(group.getAttributeNS(null, "id"), Integer.MAX_VALUE);
canvas.setSVGDocument(document);
}
public void addOverlay(final String overlayId, final int zIndex, final SVGContentGenerator generator) {
runNowOrLater(overlayId, new Runnable() {
public void run() {
addOverlayInUpdateManager(overlayId, zIndex, generator);
}
});
}
public void removeOverlay(final String overlayId) {
runNowOrLater(overlayId, new Runnable() {
public void run() {
removeOverlayInUpdateManager(overlayId);
}
});
}
public void updateOverlay(final String overlayId, final int zIndex, final SVGContentGenerator generator) {
runNowOrLater(overlayId, new Runnable() {
public void run() {
removeOverlayInUpdateManager(overlayId);
addOverlayInUpdateManager(overlayId, zIndex, generator);
}
});
}
private void runNowOrLater(String overlayId, Runnable updater) {
synchronized (lock) {
if (updateManagerIsReady) {
Log.gui.debug("Running operation on overlay " + overlayId);
canvas.getUpdateManager().getUpdateRunnableQueue().invokeLater(updater);
} else {
Log.gui.debug("Queuing operation on overlay " + overlayId);
temporaryRunnableQueue.add(updater);
}
}
}
/**
* This should only ever be called in the Update Manager thread
*
* @param overlayId
* @param generator
*/
private void addOverlayInUpdateManager(String overlayId, int zIndex, SVGContentGenerator generator) {
SVGDocument myDoc = canvas.getSVGDocument();
// StringWriter swBefore = new StringWriter();
// PrettyPrinter pp = new PrettyPrinter(myDoc.getRootElement());
// pp.prettyPrint(swBefore);
// try {
SVGElementCreator creator = new SVGElementCreator(myDoc);
SVGSVGElement root = myDoc.getRootElement();
Element group = creator.createGroup();
BoundingBox box = generator.generateContent(group, creator);
final String overlayGroupId = group.getAttributeNS(null, "id");
overlayIdMap.put(overlayId, overlayGroupId);
zIndexMap.put(overlayGroupId, zIndex);
Log.gui.debug(String.format("Overlay name %s groupId %s zIndex %d", overlayId, overlayGroupId, zIndex));
overlayBoundingBoxes.put(overlayId, box);
currentBoundingBox = currentBoundingBox.expandToInclude(box);
NodeList children = root.getChildNodes();
int childCount = children.getLength();
Log.gui.debug("Root has " + childCount + " children");
Node childToInsertBefore = null;
for (int i=0; i< childCount; i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
Element childEl = (Element) child;
if ("g".equals(childEl.getTagName())) {
childToInsertBefore = childEl;
String id = childEl.getAttributeNS(null, "id");
Log.gui.debug("Child "+ i + " has Id " + id);
int zIndexOfChild = zIndexMap.get(id);
if (zIndexOfChild > zIndex) {
break;
}
}
}
}
if (childToInsertBefore != null) {
root.insertBefore(group, childToInsertBefore);
} else {
root.appendChild(group);
}
String viewBox = currentBoundingBox.forSvg();
root.setAttributeNS(null, "viewBox", viewBox);
root.setAttributeNS(null, "overflow", "visible");
// StringWriter swAfter = new StringWriter();
// PrettyPrinter pp1 = new PrettyPrinter(myDoc.getRootElement());
// pp1.prettyPrint(swAfter);
// Log.gui.debug("Before: " + swBefore.toString());
// Log.gui.debug("After: " + swAfter.toString());
// } catch (Exception e) {
// Log.gui.warn(e);
// Log.gui.debug("Before: " + swBefore.toString());
// }
}
/**
* This should only ever be called in the Update Manager thread
*
* @param overlayId
*/
private void removeOverlayInUpdateManager(String overlayId) {
String overlayGroupId = overlayIdMap.remove(overlayId);
if (overlayGroupId != null) {
zIndexMap.remove(overlayGroupId);
SVGDocument myDoc = canvas.getSVGDocument();
SVGSVGElement root = myDoc.getRootElement();
Element el = root.getElementById(overlayGroupId);
if (el != null) {
root.removeChild(el);
}
overlayBoundingBoxes.remove(overlayGroupId);
BoundingBox newBoundingBox = rootBoundingBox;
for (BoundingBox box : overlayBoundingBoxes.values()) {
newBoundingBox = newBoundingBox.expandToInclude(box);
}
currentBoundingBox = newBoundingBox;
String viewBox = currentBoundingBox.forSvg();
root.setAttributeNS(null, "viewBox", viewBox);
} else {
Log.gui.debug("Overlay hasn't been set");
}
}
public void showProgressBar() {
progressPanel.setVisible(true);
}
public void updateProgressBar(int value) {
progressBar.setValue(value);
}
public void hideProgressBar() {
progressPanel.setVisible(false);
}
// Delegate methods for Settings panel
public JSlider getMinimumSquareSizeSlider() {
return settingsPanel.getMinimumSquareSizeSlider();
}
public JSlider getMaximumSquareSizeSlider() {
return settingsPanel.getMaximumSquareSizeSlider();
}
public JSlider getAngleSlider() {
return settingsPanel.getAngleSlider();
}
public JSlider getResolutionSlider() {
return settingsPanel.getResolutionSlider();
}
public JSlider getDisplacementSlider() {
return settingsPanel.getDisplacementSlider();
}
public JComboBox getResolutionIteratorList() {
return settingsPanel.getResolutionIteratorList();
}
public JList getResolutionList() {
return settingsPanel.getResolutionList();
}
public void update(SquareCountingResult result) {
resultPanel.update(result);
}
public void addResultPanelListener(ResultPanelListener listener) {
resultPanel.addResultPanelListener(listener);
}
public void removeResultPanelListener(ResultPanelListener listener) {
resultPanel.removeResultPanelListener(listener);
}
public void enableAllControls() {
settingsPanel.enableAllControls();
}
public void disableAllControls() {
settingsPanel.disableAllControls();
}
}